1
  • 检测Web客户端的手段很多,各有利弊,但不到万不得已就不要使用客户端检测。只要能找到更通用的方法,就应该优先采用更通用的方法。一言蔽之,先设计最通用的方案,然后再使用特定于浏览器的技术增强方案

能力检测

  • 能力检测(又称特性检测),是广泛为人接受的客户端检测形式,目标不是识别特定的浏览器,而是识别浏览器的能力。
  • IE5.0之前的版本不支持document.getElementById()这个DOM方法,尽管可以使用非标准的document.all属性实现相同的目的。于是就有类似下面的能力检测代码
function getElement(id) {
  if (document.getElementById) {
    return document.getElementById(id);
  } else if (document.all) {
    return document.all[id];
  } else {
    throw new Error("No way to retrieve element!");
  }
}
  • 一个特性存在,不一定另一个特性也存在
function getWindowWidth() {
  if (document.all) {
    // 假设这里是IE浏览器
    return document.documentElement.clientWidth;     // 错误的用法
  } else {
    return window.innerWidth;
  }
}

更可靠的能力检测

// 这不是能力检测——只是检测了是否存在相应的方法
function isSortable(object) {
  return !!object.sort;
}

// 任何包含sort属性的对象都会返回true
var result = isSortable({sort: true});
// 这样更好:检测sort是不是函数
function isSortable(object) {
  return typeof object.sort == "function";
}
  • 在可能的情况下,尽量使用typeof操作符进行能力检测。特别是,宿主对象没有义务让typeof返回合理的值。最令人发指的事就发生在 IE 中。大多数浏览器在检测到document.createElement()存在时,都会返回true
// 在IE8及之前版本不行
function hasCreateElement() {
  return typeof document.createElement == "function";
}
  • IE8- 中这个函数返回false,因为typeof document.createElement返回的的是“object”,而不是“function”。如前所述,DOM对象是宿主对象,IE及更早版本中的宿主对象是通过COM而非JScript实现的。因此,document.createElement()函数确实是一个COM对象。IE9纠正了这个问题,对所有DOM方法都返回"function"。

能力检测,不是浏览器检测

// 还不够具体
var isFirefox = !!(navigator.vendor && navigator.vendorSub);

// 假设过头了
var isIE = !!(document.all && document.uniqueID);
  • 检测某个或几个属性并不能够确定浏览器。navigator.vendornavigator.vendorSub确实是Firefox的独有属性,但是后来Safari也依样画葫芦实现了相同的属性。document.all && document.uniqueID这两个属性是早期IE的独有属性,目前还存在,但不保证未来IE不会去掉。
  • 根据浏览器不同将能力组合起来是更可取的方式。如果你知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所有相关特性,而不是分别检测。
// 确定浏览器是否支持 Netscape风格的插件
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);

// 确定浏览器是否具有DOM1级规定的能力
var hasDOM1 = !!(document.getElementById && document.createElement && document.getElementsByTagName);
  • 在实际开发中,应该将能力检测作为确定下一步解决方案的依据,而不是用它来判断用户使用的是什么浏览器。

怪癖检测

  • 怪癖检测 (quirks detection) 的目标是识别浏览器的特殊行为。但与能力检测不同,怪癖检测是想知道浏览器存在什么缺陷(也就是bug)。
  • 例如IE8及以前版本存在一个bug,即如果某个实例属性与[[Enumerbale]]标记为false的某个原型属性同名,那么该实例将不会出现在for-in循环中。
// 检测上述怪癖的代码
var hasDontEnumQuirk = function() {

  var o = { toString: function() {} };
  for (var prop in o) {
    if (prop == "toString") {
      return false;
    }
  }
  return true;
}
  • 另一个经常需要检测的怪癖是Safari 3 以前版本会枚举被隐藏的属性。
var hasEnumShadowsQuirk = function() {

  var o = { toString: function() {} };
  var count = 0;
  for (var prop in o) {
    if (prop == "toString") {
      count++;
    }
  }
  // 如果浏览器存在这个bug
  // 就会返回两个 toString 的实例
  return (count > 1);
}
  • 由于检测怪癖涉及运行代码,因此建议仅检测那些对你有直接影响的怪癖,而且最好在脚本一开始就执行此类检测。

用户代理检测

  • 用户代理检测是争议最大的客户端检测技术。
  • 用户代理检测通过用户代理字符串来确定实际使用的浏览器。在每一次HTTP请求过程中,用户代理字符串是作为响应首部发送的,而且该字符串可以通过JavaScript的navigator.userAgent属性访问。
  • 在服务端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用的而且广为接受的做法。而在客户端,用户代理检测一般被当做一种万不得已采用的做法,其优先级排在能力检测和怪癖检测之后。
  • 有关的争议不得不提电子诈骗(spoofing)。浏览器通过在自己的用户代理字符串加入一些错误或误导信息,来达到欺骗服务器的目的。

用户代理字符串的历史

用户代理字符串检测技术

识别呈现引擎

  • 确切的纸袋浏览器的名字和版本不如确切的纸袋它使用的是什么引擎。
  • 我们要编写脚本将五大呈现引擎:IE, Gecko, WebKit, KHTML, Opera
  • 为了不在全局作用域中添加多余变量,我们将使用模块增强模式来封装检测脚本
var client = function() {
  
  var engine = {

    // 呈现引擎
    ie: 0,
    gecko: 0,
    webkit: 0,
    khtml: 0,
    opera: 0,
    // 具体的版本号
    ver: null
  };

  // 在此检测呈现引擎、平台和设备
  ...

  return {
    engine: engine
  };
}();
  • 匿名函数内定义了一个局部变量engin,包含默认设置的对象字面量,每个呈现引擎都对应着一个属性,默认值为0.如果检测到了哪个呈现引擎,那么就以浮点数值形式,将引擎的版本号写入相应的属性。而呈现引擎的完整版本(一个字符串)则被写入 ver 属性。
if (client.engine.ie) {
  // 如果是IE client.ie 应该大于0
  ...
} else if (client.engine.gecko > 1.5) {
  if (client.engine.ver === "1.8.1") {
    // 针对这个版本的操作
    ...
  }
}
  • 第一个要检测是Opera,我们不相信Opera,是因为其用户代理字符串不会将自己标识为Opera。要识别Opera,必须检测window.opera对象。Opera5+都有这个版本。在Opera7.6+中调用version()方法可以返回一个表示浏览器版本的字符串。
if (window.opera) {
  engine.ver = window.opera.version();
  engine.opera = parseFloat(engine.ver);
}
  • 第二个要检测是Webkit。因为WebKit用户代理字符串中包含"Gecko"和"KHTML"这两个子字符串,所以如果首先检测它们可能会得出错误的结论。不过“AppleWebKit"是独一无二的。
  • iPhone 6s Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
  • Chrome 74 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36
  • 由于实际的版本号可能会包含数字、小数点和字母,所以捕获组中使用了表示非空格的特殊字符 \S 。用户代理字符串中的版本号与下一部分的分隔是一个空格,因此这个模式可以保证捕获所有版本信息。
var ua = navigator.userAgent;

if (/AppleWebKit\/(\S+)/.test(ua)) {
  // \S 表示非空格的特殊字符
  // \S+ 表示不包含空格的子字符串
  // 小括号表示将此子字符串加入捕获组
  // RegExp["$1"] 表示捕获组的第一个元素,即为上面描述的子字符串
  engine.ver = RegExp["$1"];
  engine.webkit = parseFloat(engine.ver);
}
  • 接下来要测试的是KHTML。同样,字符串中也包含“Gecko”,因此在排除KHTML之前,我们无法准确检测基于GECKO的浏览器。格式与Webkit差不多。此外由于Konqueror3.1及更早版本中不包含KHTML的版本,故而就要使用Konqueror代替。
if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.khtml = parseFloat(engine.ver);
}
  • 下面检测Gecko。版本号不在Gecko后面,而是在 “rv:”后面。比如WindowsXP 下的Firefox2.0.0.11:Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11
// 在上面的字符串中实际匹配的是"rv:1.8.1.11) Gecko/20071127"
// gecko的版本号位于 rv: 与一个闭括号之间,因此为了提取出这个版本号
// [^\)]+ 就是将 rv: 之后的字符串排除 ) 闭括号 之后加入捕获组
// 正则表达式要查找所有不是闭括号的字符,还要查找字符串"Gecko/"后跟8个数字
if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.gecko = parseFloat(engine.ver);
}
  • 最后一个检测IE。IE的版本号位于字符串"MSIE"的后面、一个分号的前面
// 五个呈现引擎完整的检测代码如下
var ua = navigator.userAgent;

if (window.opera) {
  engine.ver = window.opera.version();
  engine.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.webkit = parseFloat(engine.ver);
} else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.khtml = parseFloat(engine.ver);
} else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.gecko = parseFloat(engine.ver);
} else if (/MSIE ([^;]+)/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.ie = parseFloat(engine.ver);
}

识别浏览器

  • 识别呈现引擎大多数情况下足以为我们采取正确的操作提供了依据(pc上,移动端大部分都不行)。
  • 苹果公司的Safari浏览器和谷歌的Chrome浏览器都是用Webkit作为呈现引擎,但它们的JavaScript引擎却不同。
// 我们的检测代码需要添加浏览器的检测
var client = function() {
  
  var engine = {

    // 呈现引擎
    ie: 0,
    gecko: 0,
    webkit: 0,
    khtml: 0,
    opera: 0,
    // 具体的版本号
    ver: null
  };

  var browser = {

    // 浏览器
    ie: 0,
    firefox: 0,
    safari: 0,
    konq: 0,
    opera: 0,
    chrome: 0,

    // 具体的版本号
    ver: null
  }

  // 在此检测呈现引擎、平台和设备
  ...

  return {
    engine: engine,
    browser: browser
  };
}();
  • 由于大多数浏览器与其呈现引擎密切相关,所以下面的检测浏览器代码和检测呈现引擎的代码是混合在一起的。
var ua = navigator.userAgent;

if (window.opera) {
  engine.ver = browser.ver = window.opera.version();
  engine.opera = browser.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.webkit = parseFloat(engine.ver);

  // 确定是Chrome还是Safari
  if (/Chrome\/(\S+)/.test(ua)) {
    browser.ver = RegExp["$1"];
    browser.chrome = parseFloat(browser.ver);
  } else if (/Version\/(\S+)/.test(ua)) {
    browser.ver = RegExp["$1"];
    browser.safari = parseFloat(browser.ver);
  } else {
    // 近似的确定版本号
    var safariVersion = 1;
    if (engine.webkit < 100) {
      safariVersion = 1;
    } else if (engine.webkit < 312) {
      safariVersion = 1.2;
    } else if (engine.webkit < 412) {
      safariVersion = 1.3;
    } else {
      safariVersion = 2;
    }

    browser.safari = browser.ver = safariVersion;
  }
} else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
  engine.ver = browser.ver = RegExp["$1"];
  engine.khtml = browser.konq = parseFloat(engine.ver);
} else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.gecko = parseFloat(engine.ver);

  // 确定是不是Firefox浏览器
  if (/Firefox\/(\S+)/.test(ua)) {
    browser.ver = RegExp["$1"];
    browser.firefox = parseFloat(browser.ver);
  }
} else if (/MSIE ([^;]+)/.test(ua)) {
  engine.ver = RegExp["$1"];
  engine.ie = parseFloat(engine.ver);
}
  • 有了上述代码之后,我们就可以编写以下逻辑
if (client.engine.webkit) {
  if (client.browser.chrome) {
    // 执行针对Chrome的代码
  } else if (client.browser.safari) {
    // 执行针对Safari的代码
  }
} else if (client.engine.gecko) {
  if(client。browser.Firefox) {
    // 执行针对Firefox的代码
  } else {
    // 执行针对其他Gecko浏览器代码
  }
}

识别平台

  • 浏览器针对不同平台会有不同的版本,如Safari Firefox Opera , 在不同平台下可能会有不同问题。目前三大主流平台是 Window/Mac/Unix(包括各种Linux)。
// 我们的检测代码需要添加平台的检测
var client = function() {
  
  var engine = {

    // 呈现引擎
    ie: 0,
    gecko: 0,
    webkit: 0,
    khtml: 0,
    opera: 0,
    // 具体的版本号
    ver: null
  };

  var browser = {

    // 浏览器
    ie: 0,
    firefox: 0,
    safari: 0,
    konq: 0,
    opera: 0,
    chrome: 0,

    // 具体的版本号
    ver: null
  }

  var system = {
    win: false,
    mac: false,
    // Unix
    x11: false
  }

  // 在此检测呈现引擎、平台和设备
  ...

  return {
    engine: engine,
    browser: browser,
    system: system
  };
}();
  • 检测navigator.platform来确定平台,在不同浏览器中给出的值都是一致的,检测起来非常直观
var p = navigator.platform;
// window 可能有 Win32 和 Win64
system.win = p.indexOf("Win") == 0;
system.win = p.indexOf("Mac") == 0;
system.win = (p.indexOf("U11") == 0) || (p.indexOf("Linux") == 0);

识别Windows系统

  • 在WindowsXP之前,Windows有分别针对家庭和商业用户的两个版本。针对家庭的分别是Windows95和WindowsME。针对商业的一直叫WindowsNT,最后由于市场原因改名为Windows2000。这两个产品线后来又合并成一个由WindowsNT发展而来的公共代码基,代表产品就是WindowsXP。
  • 原著编撰年代较早(2012.3),笔记这里只列举WindowsXP以后的代码,并补充Windows10
版本 IE 4+ Gecko Opera < 7 Opera 7+ WebKit
XP "Windows NT 5.1" "Windows NT 5.1" "WindowsXP" "Windows NT 5.1" "Windows NT 5.1"
Vista "Windows NT 6.0" "Windows NT 6.0" n/a "Windows NT 6.0" "Windows NT 6.0"
7 "Windows NT 6.1" "Windows NT 6.1" n/a "Windows NT 6.1" "Windows NT 6.1"
10 "Windows NT 10.0" "Windows NT 10.0" n/a "Windows NT 10.0" "Windows NT 10.0"
  • 忽略掉Opera 7- 的用户和WindowsXP以前的系统,将原著代码修改如下:
if (system.win) {
  // 比如在Windows10的Chrome里,userAgent返回字符串
  // Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
  // 我们要提取两个子字符串 'NT' 和 '10.0'
  // ([^dows]{2}) 表示排除包含'dows'的字符之后的后2位字符 加入捕获组 $1
  // (\d+\.\d+) 表示 x.x(x) 形式的版本号加入捕获组 $2
  if (/Windows ([^dows]{2})\s?(\d+\.\d+)?/.test(ua)) {
    if (RegExp["$1"] == "NT") {
      switch (RegExp["$2"]) {
        case "5.1":
          system.win = "XP";
          break;
        case "6.0":
          system.win = "Vista";
          break;
        case "6.1":
          system.win = "7";
          break;
        case "10.0":
          system.win = "10";
          break;
        default:
          system.win = "NT";
          break;
      }
    } else {
      system.win = RegExp["$1"];
    }
  }
}

识别移动设备

// 我们在系统变量里添加移动设备的属性
var client = function() {
  ...

  var system = {
    win: false,
    mac: false,
    // Unix
    x11: false,

    // 移动设备
    iphone: false,
    ipod: false,
    ipad: false,
    ios: false,
    android: false,
    nokiaN: false,
    winMobile: false
  }

  // 在此检测呈现引擎、平台和设备
  ...

  return {
    engine: engine,
    browser: browser,
    system: system
  };
}();
  • 通常的检测字符串"iphone", "ipod", "ipad", 就可以分别设置相应属性的值了。
system.iphone = ua.indexOf("iPhone") > -1;
system.ipod = ua.indexOf("iPod") > -1;
system.ipad = ua.indexOf("iPad") > -1;
  • 除了知道iOS设备,最好还能知道iOS的版本号。在iOS3之前,用户代理字符串只包含"CPU like Mac OS",后来iPhone中又改成"CPU iPhone OS 3_0 like Mac OS X",iPad中又改成"CPU OS 3_2 like Mac OS X"。
  • 检查系统是不是 Mac OS、字符串中是否存在"Mobile",可以保证不论是什么版本,system.ios中都不会是 0
// 检测iOS版本
if (system.max && ua.indexOf("Mobile") > -1) {
  if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)) {
    system.ios = parseFloat(RegExp.$1.repalce("_", "."));
  } else {
    system.ios = 2;   // 不能准确判断只能靠猜
  }
}
  • 检测Android操作系统也很简单,搜索字符串"Android"并取得紧跟其后的版本号
// 检测Android版本
if (/Android (\d+\.\d+)/.test(ua)) {
  system.android = parseFloat(RegExp.$1);
}
  • 远在天国的诺基亚,略
  • 在天国门口的Windows Phone , 略

识别游戏系统

  • 除了移动设备之外,视频游戏系统中的Web浏览器也开始日益普及。任天堂Wii和PlayStation3+等等。
  • Wii的浏览器实际上是定制版的Opera,专门为Wii Remote设计的。用户代理字符串:Opera/9.10 (Nintendo Wii;U; ; 1621; en)
  • PlayStation的浏览器是自己开发的,没有基于前面提到的任何呈现引擎。用户代理字符串:Mozilla/5.0 (PLAYSTATION 3; 2.00)
// 我们在系统变量里添加游戏设备的属性
var client = function() {
  ...

  var system = {
    ...

    // 游戏系统
    wii: false,
    ps: false
  }

  // 在此检测呈现引擎、平台和设备
  ...

  return {
    engine: engine,
    browser: browser,
    system: system
  };
}();
  • 检测前述游戏系统的代码如下
system.wii = ua.indexOf("Wii") > -1;
// ps要忽略大小写
system.ps = /playstation/i.test(ua);

完整的代码

// 个人做了部分修改 修改时间 2019.5.17
var client = function() {
  
  var engine = {

    // 呈现引擎
    ie: 0,
    gecko: 0,
    webkit: 0,
    khtml: 0,
    opera: 0,
    // 具体的版本号
    ver: null
  };

  var browser = {

    // 浏览器
    ie: 0,
    firefox: 0,
    safari: 0,
    konq: 0,
    opera: 0,
    chrome: 0,

    // 具体的版本号
    ver: null
  }

  var system = {
    win: false,
    mac: false,
    // Unix
    x11: false,

    // 移动设备
    iphone: false,
    ipod: false,
    ipad: false,
    ios: false,
    android: false,
    nokiaN: false,
    winMobile: false,

    // 游戏系统
    wii: false,
    ps: false
  }

  // 在此检测呈现引擎、平台和设备
  var ua = navigator.userAgent;

  if (window.opera) {
    engine.ver = browser.ver = window.opera.version();
    engine.opera = browser.opera = parseFloat(engine.ver);
  } else if (/AppleWebKit\/(\S+)/.test(ua)) {
    engine.ver = RegExp["$1"];
    engine.webkit = parseFloat(engine.ver);

    // 确定是Chrome还是Safari
    if (/Chrome\/(\S+)/.test(ua)) {
      browser.ver = RegExp["$1"];
      browser.chrome = parseFloat(browser.ver);
    } else if (/Version\/(\S+)/.test(ua)) {
      browser.ver = RegExp["$1"];
      browser.safari = parseFloat(browser.ver);
    } else {
      // 近似的确定版本号
      var safariVersion = 1;
      if (engine.webkit < 100) {
        safariVersion = 1;
      } else if (engine.webkit < 312) {
        safariVersion = 1.2;
      } else if (engine.webkit < 412) {
        safariVersion = 1.3;
      } else {
        safariVersion = 2;
      }

      browser.safari = browser.ver = safariVersion;
    }
  } else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
    engine.ver = browser.ver = RegExp["$1"];
    engine.khtml = browser.konq = parseFloat(engine.ver);
  } else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)) {
    engine.ver = RegExp["$1"];
    engine.gecko = parseFloat(engine.ver);

    // 确定是不是Firefox浏览器
    if (/Firefox\/(\S+)/.test(ua)) {
      browser.ver = RegExp["$1"];
      browser.firefox = parseFloat(browser.ver);
    }
  } else if (/MSIE ([^;]+)/.test(ua)) {
    engine.ver = RegExp["$1"];
    engine.ie = parseFloat(engine.ver);
  }

  // 检测浏览器
  browser.ie = engine.ie;
  browser.opera = engine.opera;

  // 检测平台
  var p = navigator.platform;
  system.win = p.indexOf("Win") == 0;
  system.win = p.indexOf("Mac") == 0;
  system.win = (p.indexOf("U11") == 0) || (p.indexOf("Linux") == 0);

  // 检测Windos操作系统
  // 排除WindosXP以前的系统
  if (system.win) {
    if (/Windows ([^dows]{2})\s?(\d+\.\d+)?/.test(ua)) {
      if (RegExp["$1"] == "NT") {
        switch (RegExp["$2"]) {
          case "5.1":
            system.win = "XP";
            break;
          case "6.0":
            system.win = "Vista";
            break;
          case "6.1":
            system.win = "7";
            break;
          case "10.0":
            system.win = "10";
            break;
          default:
            system.win = "NT";
            break;
        }
      } else {
        system.win = RegExp["$1"];
      }
    }
  }

  // 移动设备
  // 剔除了诺基亚和windows phone
  system.iphone = ua.indexOf("iPhone") > -1;
  system.ipod = ua.indexOf("iPod") > -1;
  system.ipad = ua.indexOf("iPad") > -1;

  // 检测iOS版本
  if (system.max && ua.indexOf("Mobile") > -1) {
    if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)) {
      system.ios = parseFloat(RegExp.$1.repalce("_", "."));
    } else {
      system.ios = 2;   // 不能准确判断只能靠猜
    }
  }

  // 检测Android版本
  if (/Android (\d+\.\d+)/.test(ua)) {
    system.android = parseFloat(RegExp.$1);
  }

  // 游戏系统
  system.wii = ua.indexOf("Wii") > -1;
  system.ps = /playstation/i.test(ua);

  return {
    engine: engine,
    browser: browser,
    system: system
  };
}();

使用方法

  • 用户代理检测是客户端检测的最后一个选项。只要可能,都应该优先采用能力检测和怪癖检测。用户代理检测一般适用于下列情形:

    • 不能直接准确的使用能力检测或怪癖检测。例如某些浏览器实现了为将来功能预留的存根函数(stub)。在这种情况下,仅测试相应的函数是否存在还得不到足够信息。
    • 同一款浏览器在不同平台下具备不同的能力。
    • 为了跟踪分析等目的需要知道确切的浏览器。

maroonstar
11 声望3 粉丝